package com.itany.util;

import com.itany.exception.DataAccessException;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author:石小俊
 * JDBC模板方法:对JDBC访问数据库的操作进行封装,简化开发
 */
public class JDBCTemplate<T> {

    /**
     * 执行一个DML操作,即:insert、update、delete
     * 对于增删改操作,都是使用ps.executeUpdate()执行sql
     * 对于整个方法而言,主要在于sql与sql参数不一致,其他基本一致
     * 可以将不一致的sql与sql参数封装成模板方法的参数
     * 在实际调用时去传递需要的sql与sql参数
     * 由于参数无法确定是否有,有几个,可以使用数组或者可变长参数进行封装
     * @param sql       SQL语句
     * @param params    SQL中的参数
     */
    public void update(String sql,Object... params){
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            conn = JDBCUtil.getConnection();
            ps = conn.prepareStatement(sql);
            // 设置参数
            // 遍历可变长参数,有几个参数,就设置几次
            for(int i = 0; i < params.length; i++){
                // 取出当前遍历的参数
                Object param = params[i];
                // 此时无法确定调用者参数的类型
                // 可以通过判断来处理
                if(param instanceof String){
                    ps.setString(i+1, (String) param);
                }else if(param instanceof Integer){
                    ps.setInt(i+1, (Integer) param);
                }else if(param instanceof Double){
                    ps.setDouble(i+1, (Double) param);
                }else{
                    // 也可以直接通过setObject表示所有类型的参数
                    ps.setObject(i+1,param);
                }
            }
            // 执行sql
            ps.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
            throw new DataAccessException("数据访问异常");
        } finally {
            JDBCUtil.close(conn,ps,null);
        }
    }

    /**
     * 执行一个查询操作
     * 对于查询操作而言,还需要对查询出来的结果集进行映射
     * 具体怎么映射只有调用者在调用时才知道
     * 创建一个接口作为模板方法的参数,调用者在调用时对该接口做具体实现
     * 方法的返回值可能是一个对象,也可能是一个对象集合
     * 通过集合均可以接收到,如果是对象,值即为集合中的第一条数据
     * @param sql   SQL语句
     * @param rm    行映射,将查询出来的结果集映射成具体的Java对象
     * @param params SQL语句中的参数
     * @return  根据RowMapper的具体实现返回具体的对象集合
     */
    public List<T> query(String sql, RowMapper rm, Object... params){
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        List<T> list = new ArrayList<>();
        try {
            conn = JDBCUtil.getConnection();
            ps = conn.prepareStatement(sql);
            // 设置参数
            // 遍历可变长参数,有几个参数,就设置几次
            for(int i = 0; i < params.length; i++){
                // 取出当前遍历的参数
                Object param = params[i];
                // 此时无法确定调用者参数的类型
                // 可以通过判断来处理
                if(param instanceof String){
                    ps.setString(i+1, (String) param);
                }else if(param instanceof Integer){
                    ps.setInt(i+1, (Integer) param);
                }else if(param instanceof Double){
                    ps.setDouble(i+1, (Double) param);
                }else{
                    // 也可以直接通过setObject表示所有类型的参数
                    ps.setObject(i+1,param);
                }
            }
            // 执行查询操作,返回结果集
            rs = ps.executeQuery();
            while(rs.next()){
                T t = (T) rm.mapRow(rs);
                list.add(t);
            }
            return list;
        } catch (Exception e) {
            e.printStackTrace();
            throw new DataAccessException("数据访问异常");
        } finally {
            JDBCUtil.close(conn,ps,rs);
        }
    }

    /**
     * 执行DML语句,用于保存数据的同时返回生成的主键值
     * 此时生成PreparedStatement对象的方式与之前的不一致
     * 因此,可以定义一个用于生成PreparedStatement对象的接口,由调用者具体实现
     * 返回的主键值可能是一个,也可能是多个,因此定义一个用于接收返回的主键数据的对象KeyHolder
     * @param psc           用于构建PreparedStatement
     * @param keyHolder    用于存储生成的主键
     */
    public void update(PreparedStatementCreator psc, KeyHolder keyHolder){
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            conn = JDBCUtil.getConnection();
            // 调用者在调用时做具体实现
            ps = psc.createPreparedStatement(conn);
            // 执行保存操作
            ps.executeUpdate();

            // 获取自动生成的键
            rs = ps.getGeneratedKeys();
            // 创建一个集合,用于存放所有生成的键
            // 一般使用Object,但是我们实际应用时一般是int类型
            List<Integer> list = new ArrayList();

            // 获取结果集元数据
            ResultSetMetaData rsmd = rs.getMetaData();
            // 通过结果集元数据获取生成的列的总数
            int columnCount = rsmd.getColumnCount();
            // 读取生成的所有键的值
            if(rs.next()){
                // 在按行读取数据的时候,遍历所有列的值
                // 将每一列的值存放到集合中
                // 每一列的值对应一个主键值
                for(int i = 1; i <= columnCount; i++){
                    list.add(rs.getInt(i));
                }
            }
            keyHolder.setKeyList(list);
        } catch (Exception e) {
            e.printStackTrace();
            throw new DataAccessException("数据访问异常");
        } finally {
            JDBCUtil.close(conn,ps,rs);
        }
    }
}
